/**
 * PageMarket HttpManager.java com.android.pagemarket.io.protocol
 * 
 */
package com.android.pagemarket.io.protocol;

import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.UnsupportedEncodingException;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.zip.GZIPInputStream;

import javax.net.ssl.SSLHandshakeException;

import org.apache.http.Header;
import org.apache.http.HeaderElement;
import org.apache.http.HttpEntity;
import org.apache.http.HttpRequest;
import org.apache.http.HttpRequestInterceptor;
import org.apache.http.HttpResponse;
import org.apache.http.HttpResponseInterceptor;
import org.apache.http.HttpVersion;
import org.apache.http.NoHttpResponseException;
import org.apache.http.StatusLine;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CookieStore;
import org.apache.http.client.HttpRequestRetryHandler;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.params.ClientPNames;
import org.apache.http.client.protocol.ClientContext;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.conn.params.ConnManagerParams;
import org.apache.http.conn.params.ConnPerRouteBean;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.entity.BufferedHttpEntity;
import org.apache.http.entity.HttpEntityWrapper;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.apache.http.params.HttpProtocolParams;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.ExecutionContext;
import org.apache.http.protocol.HttpContext;
import org.apache.http.protocol.SyncBasicHttpContext;
import org.apache.http.util.EntityUtils;

import android.os.SystemClock;

import com.android.pagemarket.io.protocol.in.HttpAction;
import com.android.pagemarket.util.Log;

/*********************************************************
 * @author : zhaohua
 * @version : 2012-11-3
 * @see : Http协议管理器
 * @Copyright : copyrights reserved by personal 2007-2012
 **********************************************************/
public class HttpManager
{
    private static final String TAG = "HttpManager";

    private static HttpManager instance;

    private static String ENCODING = "UTF-8";

    private static final int DEFAULT_MAX_CONNECTIONS = 100;

    private static final int DEFAULT_SOCKET_TIMEOUT = 10 * 1000;

    private static final int DEFAULT_MAX_RETRIES = 2;

    private static final int DEFAULT_SOCKET_BUFFER_SIZE = 8192;

    private static final String HEADER_ACCEPT_ENCODING = "Accept-Encoding";

    private static final String ENCODING_GZIP = "gzip";

    private static int maxConnections = DEFAULT_MAX_CONNECTIONS;

    private static int socketTimeout  = DEFAULT_SOCKET_TIMEOUT;

    private final DefaultHttpClient httpClient;

    private final HttpContext httpContext;

    private ThreadPoolExecutor threadPool;

    private final Map<String, String> clientHeaderMap;

    private HttpManager()
    {
        BasicHttpParams httpParams = new BasicHttpParams();

        ConnManagerParams.setTimeout(httpParams, socketTimeout);
        ConnManagerParams.setMaxConnectionsPerRoute(httpParams,new ConnPerRouteBean(maxConnections));
        ConnManagerParams.setMaxTotalConnections(httpParams,DEFAULT_MAX_CONNECTIONS);

        HttpConnectionParams.setSoTimeout(httpParams, socketTimeout);
        HttpConnectionParams.setConnectionTimeout(httpParams, socketTimeout);
        HttpConnectionParams.setTcpNoDelay(httpParams, true);
        HttpConnectionParams.setSocketBufferSize(httpParams,DEFAULT_SOCKET_BUFFER_SIZE);
        // 设置为循环重定向不检测
        httpParams.setBooleanParameter(ClientPNames.ALLOW_CIRCULAR_REDIRECTS, true);
        HttpProtocolParams.setVersion(httpParams, HttpVersion.HTTP_1_1);
        HttpProtocolParams
                .setUserAgent(
                        httpParams,
                        "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.4 (KHTML, like Gecko) Chrome/22.0.1229.94 Safari/537.4");
        SchemeRegistry schemeRegistry = new SchemeRegistry();
        schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
        schemeRegistry.register(new Scheme("https", SSLSocketFactory.getSocketFactory(), 443));
        ThreadSafeClientConnManager cm = new ThreadSafeClientConnManager(httpParams, schemeRegistry);

        httpContext = new SyncBasicHttpContext(new BasicHttpContext());
        httpClient = new DefaultHttpClient(cm, httpParams);
        httpClient.addRequestInterceptor(new HttpRequestInterceptor()
        {
            public void process(HttpRequest request, HttpContext context)
            {
                if (!request.containsHeader(HEADER_ACCEPT_ENCODING))
                {
                    request.addHeader(HEADER_ACCEPT_ENCODING, ENCODING_GZIP);
                }
                for (String header : clientHeaderMap.keySet())
                {
                    request.addHeader(header, clientHeaderMap.get(header));
                }
            }
        });

        httpClient.addResponseInterceptor(new HttpResponseInterceptor()
        {
            public void process(HttpResponse response, HttpContext context)
            {
                final HttpEntity entity = response.getEntity();
                if (entity == null) { return; }
                final Header encoding = entity.getContentEncoding();
                if (encoding != null)
                {
                    for (HeaderElement element : encoding.getElements())
                    {
                        if (element.getName().equalsIgnoreCase(ENCODING_GZIP))
                        {
                            response.setEntity(new InflatingEntity(response
                                    .getEntity()));
                            break;
                        }
                    }
                }
            }
        });

        httpClient.setHttpRequestRetryHandler(new RetryHandler(DEFAULT_MAX_RETRIES));
        
        threadPool = (ThreadPoolExecutor) Executors.newCachedThreadPool();
        clientHeaderMap = new HashMap<String, String>();
    }

    public static HttpManager getInstance()
    {
        if (instance == null)
        {
            instance = new HttpManager();
        }
        return instance;
    }

    public void setCookieStore(CookieStore cookieStore)
    {
        httpContext.setAttribute(ClientContext.COOKIE_STORE, cookieStore);
    }

    public void setUserAgent(String userAgent)
    {
        HttpProtocolParams.setUserAgent(this.httpClient.getParams(), userAgent);
    }

    public void setTimeout(int timeout)
    {
        final HttpParams httpParams = this.httpClient.getParams();
        ConnManagerParams.setTimeout(httpParams, timeout);
        HttpConnectionParams.setSoTimeout(httpParams, timeout);
        HttpConnectionParams.setConnectionTimeout(httpParams, timeout);
    }

    public void setSSLSocketFactory(SSLSocketFactory sslSocketFactory)
    {
        this.httpClient.getConnectionManager().getSchemeRegistry()
                .register(new Scheme("https", sslSocketFactory, 443));
    }

    public void addHeader(String header, String value)
    {
        clientHeaderMap.put(header, value);
    }

    public void setBasicAuth(String user, String pass)
    {
        AuthScope scope = AuthScope.ANY;
        setBasicAuth(user, pass, scope);
    }

    public void setBasicAuth(String user, String pass, AuthScope scope)
    {
        UsernamePasswordCredentials credentials = new UsernamePasswordCredentials(
                user, pass);
        this.httpClient.getCredentialsProvider().setCredentials(scope,
                credentials);
    }

    private HttpResponse sendRequest(HttpUriRequest uriRequest)
    {
        boolean retry = true;
        IOException cause = null;
        int executionCount = 0;
        RetryHandler retryHandler = (RetryHandler)httpClient.getHttpRequestRetryHandler();
        while (retry)
        {
            try
            {
                Log.d(TAG, "-->sendRequest" + uriRequest.getURI());
                retryHandler.setNoRetry(false);
                return httpClient.execute(uriRequest, httpContext);
            } catch (IOException e)
            {
                retryHandler.setNoRetry(true);
                retry = retryHandler.retryRequest(e, ++executionCount, httpContext);
            } catch (NullPointerException e)
            {
                // there's a bug in HttpClient 4.0.x that on some occasions causes
                // DefaultRequestExecutor to throw an NPE, see
                // http://code.google.com/p/android/issues/detail?id=5255
                cause = new IOException("NPE in HttpClient" + e.getMessage());
                retryHandler.setNoRetry(true);
                retry = retryHandler.retryRequest(cause, ++executionCount, httpContext);
            }
        }
        return null;
    }

    String getTextResponse(HttpResponse response)
    {
        if (response == null) return null;
        StatusLine status = response.getStatusLine();
        if (status.getStatusCode() >= 300)
        {
            Log.e(TAG,
                    status.getStatusCode() + ":\n" + status.getReasonPhrase());
            return null;
        } else
        {
            try
            {
                HttpEntity entity = null;
                HttpEntity temp = response.getEntity();
                if (temp != null)
                {
                    entity = new BufferedHttpEntity(temp);
                    return EntityUtils.toString(entity, ENCODING);
                }
                return "";
            } catch (IOException e)
            {
                return null;
            }
        }
    }

    InputStream getFileResponse(HttpResponse response)
    {
        StatusLine status = response.getStatusLine();
        Header[] contentTypeHeaders = response.getHeaders("Content-Type");
        if (contentTypeHeaders.length != 1)
        {
            Log.e(TAG, status.getStatusCode()
                    + ": None, or more than one, Content-Type Header found!");
            return null;
        }
        if (status.getStatusCode() >= 300)
        {
            Log.e(TAG,
                    status.getStatusCode() + ":\n" + status.getReasonPhrase());
            return null;
        } else
        {
            try
            {
                HttpEntity entity = null;
                HttpEntity temp = response.getEntity();
                if (temp != null)
                {
                    entity = new BufferedHttpEntity(temp);
                }
                return entity.getContent();
            } catch (IOException e)
            {
                e.printStackTrace();
                return null;
            }
        }
    }

    HttpResponse get(String url, Header[] headers,
            HashMap<String, String> params)
    {
        HttpUriRequest request = new HttpGet(getUrlWithQueryString(url, params));
        if (headers != null) request.setHeaders(headers);
        return sendRequest(request);
    }

    HttpResponse post(String url, Header[] headers,
            HashMap<String, String> params)
    {
        HttpEntityEnclosingRequestBase request = new HttpPost(url);
        if (params != null) request.setEntity(paramsToEntity(params));
        if (headers != null) request.setHeaders(headers);
        return sendRequest(request);
    }

    HttpResponse post(String url, Header[] headers, HttpEntity entity)
    {
        HttpEntityEnclosingRequestBase request = addEntityToRequestBase(
                new HttpPost(url), entity);
        if (headers != null) request.setHeaders(headers);
        return sendRequest(request);
    }

    public boolean submitAction(HttpAction<?> action)
    {
        if (action == null || !action.isValid()) return false;
        try
        {
            Future<?> furture = threadPool.submit(new ActionTask(action));
            action.setFuture(furture);
            return true;
        } catch (Exception e)
        {
            e.printStackTrace();
        }
        return false;
    }

    public static String getUrlWithQueryString(String url,
            HashMap<String, String> params)
    {
        if (params != null)
        {
            String paramString = URLEncodedUtils.format(getParamsList(params),
                    ENCODING);
            if (url.contains("?"))
            {
                url += "&" + paramString;
            } else
            {
                url += "?" + paramString;
            }
        }

        return url;
    }

    protected static HttpEntity paramsToEntity(HashMap<String, String> params)
    {
        HttpEntity entity = null;

        if (params != null)
        {
            try
            {
                entity = new UrlEncodedFormEntity(getParamsList(params),
                        ENCODING);
            } catch (UnsupportedEncodingException e)
            {
                e.printStackTrace();
            }
        }

        return entity;
    }

    protected static List<BasicNameValuePair> getParamsList(
            HashMap<String, String> params)
    {
        List<BasicNameValuePair> lparams = new LinkedList<BasicNameValuePair>();

        for (ConcurrentHashMap.Entry<String, String> entry : params.entrySet())
        {
            lparams.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));
        }

        return lparams;
    }

    protected HttpEntityEnclosingRequestBase addEntityToRequestBase(
            HttpEntityEnclosingRequestBase requestBase, HttpEntity entity)
    {
        if (entity != null)
        {
            requestBase.setEntity(entity);
        }

        return requestBase;
    }

    private static class InflatingEntity extends HttpEntityWrapper
    {
        public InflatingEntity(HttpEntity wrapped)
        {
            super(wrapped);
        }

        @Override
        public InputStream getContent() throws IOException
        {
            return new GZIPInputStream(wrappedEntity.getContent());
        }

        @Override
        public long getContentLength()
        {
            return -1;
        }
    }

    private class ActionTask implements Runnable
    {
        private HttpAction<?> action;

        public ActionTask(HttpAction<?> action)
        {
            this.action = action;
        }

        @Override
        public void run()
        {
            HttpResponse rsp = null;
            if (action.isHttpGet())
            {
                rsp = get(action.getAction(), null, action.getMapParams());
            } else
            {
                rsp = post(action.getAction(), null, action.getMapParams());
            }

            if(!action.isFile())
            {
                // 文本
                String result = getTextResponse(rsp);
                if (result == null)
                {
                    action.onError(HttpAction.ERR_NETWORK);
                } else
                {
                    action.onFinish(result);
                }
            }else
            {
                // 文件
                InputStream ins = getFileResponse(rsp);
                if (ins == null)
                {
                    action.onError(HttpAction.ERR_NETWORK);
                } else
                {
                    action.onFinish(ins);
                }
            }
        }
    }
}

class RetryHandler implements HttpRequestRetryHandler
{
    private static final int RETRY_SLEEP_TIME_MILLIS = 1500;

    private static HashSet<Class<?>> exceptionWhitelist = new HashSet<Class<?>>();

    private static HashSet<Class<?>> exceptionBlacklist = new HashSet<Class<?>>();

    static
    {
        // Retry if the server dropped connection on us
        exceptionWhitelist.add(NoHttpResponseException.class);
        // retry-this, since it may happens as part of a Wi-Fi to 3G failover
        exceptionWhitelist.add(UnknownHostException.class);
        // retry-this, since it may happens as part of a Wi-Fi to 3G failover
        exceptionWhitelist.add(SocketException.class);

        // never retry timeouts
        exceptionBlacklist.add(InterruptedIOException.class);
        // never retry SSL handshake failures
        exceptionBlacklist.add(SSLHandshakeException.class);
    }

    private final int maxRetries;
    private boolean retryable = true;
    
    public RetryHandler(int maxRetries)
    {
        this.maxRetries = maxRetries;
    }
    
    public void setNoRetry(boolean retryable)
    {
        this.retryable = retryable;
    }

    public boolean retryRequest(IOException exception, int executionCount, HttpContext context)
    {
        if(!retryable) return false;
        boolean retry = true;
        Boolean b = (Boolean) context.getAttribute(ExecutionContext.HTTP_REQ_SENT);
        boolean sent = (b != null && b.booleanValue());

        if (executionCount > maxRetries)
        {
            // Do not retry if over max retry count
            retry = false;
        } else if (exceptionBlacklist.contains(exception.getClass()))
        {
            // immediately cancel retry if the error is blacklisted
            retry = false;
        } else if (exceptionWhitelist.contains(exception.getClass()))
        {
            // immediately retry if error is whitelisted
            retry = true;
        } else if (!sent)
        {
            // for most other errors, retry only if request hasn't been fully sent yet
            retry = true;
        }
        /** here post as get
        if (retry)
        {
            // resend all idempotent requests
            HttpUriRequest currentReq = (HttpUriRequest) context.getAttribute(ExecutionContext.HTTP_REQUEST);
            String requestType = currentReq.getMethod();
            retry = !requestType.equals("POST");
        }*/

        if (retry)
        {
            SystemClock.sleep(RETRY_SLEEP_TIME_MILLIS);
        } else
        {
            exception.printStackTrace();
        }

        return retry;
    }
}